# 数据获取

import React from 'react';
import ProviderTip from '@components/zh/data-fetch/provider-tip';
import Consumer from '@components/zh/data-fetch/consumer';

:::tip
Data Loader 同时支持 SSR 和 CSR 两种场景！

* [Demo](https://github.com/module-federation/core/tree/main/apps/modern-component-data-fetch)

:::

## 简介

SSR 场景中， `useEffect` 不会执行，这一行为导致常规情况下，无法先获取数据再渲染组件。

为了支持这一功能，主流框架一般会基于 React Router 提供的 `data loader` 预取数据，并注入给路由组件，路由组件通过 [useLoaderData](https://reactrouter.com/api/hooks/useLoaderData#useloaderdata) 获取数据并渲染。

这一行为强依赖路由功能，在 {props.name || 'Module Federation'} 下就无法正常使用。

为了解决这一问题，Module Federation 提供了**组件**级别数据获取能力，以便开发者可以在 SSR 场景下获取数据并渲染组件。

:::tip 什么是组件级别？
Module Federation 使用大体分为两种部分：组件（函数）、应用。两者的区别在于是否带有*路由*功能。
:::

## 如何使用

根据角色的不同，需要做不同的行为。

### 生产者

{props.providerTip || React.createElement(ProviderTip)}

每个 expose 模块都可以有一个同名的 `.data` 文件。这些文件可以导出一个 loader 函数，我们称为 Data Loader，它会在对应的 `expose` 组件渲染之前执行，为组件提供数据。如下面示例：

```bash
.
└── src
    ├── List.tsx
    └── List.data.ts
```

其中 `List.data.ts` 需要导出名为 `fetchData` 的函数，该函数将会在 `List` 组件渲染前执行，并将其数据注入，示例如下：

```ts title="List.data.ts"
import type { DataFetchParams } from '@module-federation/bridge-react';
export type Data = {
  data: string;
};

export const fetchData = async (params: DataFetchParams): Promise<Data> => {
  console.log('params: ', params);
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        data: `data: ${new Date()}`,
      });
    }, 1000);
  });
};
```

loader 函数数据会被注入到生产者的 props 中，key 为 `mfData`，因此生产者需要改动代码，消费此数据，示例如下：

```tsx title="List.tsx"
import React from 'react';
import type { Data } from './index.data';

const List = (props: {
  mfData?: Data;
}): JSX.Element => {
  return (
    <div>
     {props.mfData?.data && props.mfData?.data.map((item,index)=><p key={index}>{item}</p>)}
    </div>
  );
};

export default List;
```

#### 生产者消费自身数据

如果使用了 Modern.js 来开发生产者，并且该生产者页面也会单独访问。那么可以利用 Modern.js 提供的 [Data Loader](https://modernjs.dev/zh/guides/basic-features/data/data-fetch.html) 来注入数据。

其用法与 {props.name || 'Module Federation'} 除了函数名称外，基本一致。因此你可以很方便的在生产者消费 Data Loader，示例如下：

* 在生产者页面创建 `page.data.ts` 文件，导出名为 `loader` 的函数：

```ts title="page.data.ts"
import { fetchData } from '../components/List.data';
import type { Data } from '../components/List.data';

export const loader = fetchData

export type {Data}
```

* 在生产者 `page` 页面消费此数据：

```tsx title="page.tsx"
import { useLoaderData } from '@modern-js/runtime/router';
import List from '../components/List';
import './index.css';

import type { Data } from './page.data';

const Index = () => {
  const data = useLoaderData() as Data;
  console.log('page data', data);

  return (
  <div className="container-box">
    <List mfData={data} />
  </div>
)};

export default Index;

```

### 消费者

{props.consumer || React.createElement(Consumer)}

#### loader 函数

##### 入参

默认会往 loader 函数传递参数，其类型为 [DataFetchParams](/practice/bridge/react-bridge/load-component#datafetchparams)，包含以下字段：

- `isDowngrade` (boolean): 表示当前执行上下文是否处于降级模式。例如，在服务端渲染 (SSR) 失败，在客户端渲染（CSR）中会重新往服务端发起请求，调用 loader 函数，此时该值为 `true`。

除了默认的参数以外，还可以在 `createLazyComponent` 中传递 `dataFetchParams` 字段，该字段会被透传给 loader 函数。

##### 返回值

loader 函数的返回值只能是**可序列化的数据对象**。

#### 在不同环境使用 Data Loader

loader 函数可能会在服务端或浏览器端执行。在服务端执行的 loader 函数，我们称为 Server Loader，在浏览器端执行的称为 Client Loader。

在 CSR 应用中，loader 函数会在浏览器端执行，即默认都是 Client Loader。

在 SSR 应用中，loader 函数只会在服务端执行，即默认都是 Server Loader。在 SSR {props.name || 'Module Federation'} 会直接在服务端调用对应的 loader 函数。在浏览器端切换路由时，Module Federation 会发送一个 http 请求到 SSR 服务，同样在服务端触发 loader 函数。

:::info
SSR 应用的 loader 函数只在服务端执行可以带来以下好处：

- 简化使用方式：保证 SSR 应用获取数据的方式是同构的，开发者无需根据环境区分 loader 函数执行的代码。

- 减少浏览器端 bundle 体积：将逻辑代码及其依赖，从浏览器端移动到了服务端。

- 提高可维护性：将逻辑代码移动到服务端，减少了数据逻辑对前端 UI 的直接影响。此外，也避免了浏览器端 bundle 中误引入服务端依赖，或服务端 bundle 中误引入浏览器端依赖的问题。

:::

##### 在 SSR 应用中使用 Client Loader

默认情况下，在 SSR 应用中，loader 函数只会在服务端执行。但有些场景下，开发者可能期望在浏览器端发送的请求不经过 SSR 服务，直接请求数据源，例如：

1. 在浏览器端希望减少网络消耗，直接请求数据源。
2. 应用在浏览器端有数据缓存，不希望请求 SSR 服务获取数据。

{props.name || 'Module Federation'} 支持在 SSR 应用中额外添加 .data.client 文件，同样具名导出 loader。此时 SSR 应用在服务端执行 Data Loader 报错降级，或浏览器端切换路由时，会像 CSR 应用一样在浏览器端执行该 loader 函数，而不是再向 SSR 服务发送数据请求。

```ts title="List.data.client.ts"
import cache from 'my-cache';

export async function loader({ params }) {
  if (cache.has(params.id)) {
    return cache.get(params.id);
  }
  const res = await fetch('URL_ADDRESS?id={params.id}');
  return {
    message: res.message,
  }
}
```
:::warning WARNING

要使用 Client Loader，必须有对应的 Server Loader，且 Server Loader 必须是 .data 文件约定，不能是 .loader 文件约定。

:::

## FAQ

### 应用级别数据获取？

应用级别的模块，我们更希望使用 RSC 来实现，使其功能更加的完善。目前该功能正在探索中，敬请期待。

### 是否支持嵌套生产者？

不支持。

### 生产者除了使用 Rslib 插件或者 Modern.js 插件外还有其他的插件吗？

暂时只有 Rslib 插件和 Modern.js 插件才可以创建 Data Loader。
